package com.wjhs.redispractice.repeatsubmit.aspect;

import com.wjhs.redispractice.repeatsubmit.annotation.RepeatSubmit;
import com.wjhs.redispractice.repeatsubmit.utils.AddrUtil;
import com.wjhs.redispractice.repeatsubmit.utils.MD5Util;
import io.micrometer.common.util.StringUtils;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

/**
 * 利用AOP实现接口防重处理
 */
@Aspect
public class RepeatSubmitAspect {
    public static final String SERVICE_SUBMIT_TOKEN_KEY = "service_submit_token_key";

    @Resource
    private RedisTemplate redisTemplate;

    @Resource
    private RedissonClient redissonClient;

    @Pointcut("@annotation(repeatSubmit)")
    public void pointCutNoRepeatSubmit(RepeatSubmit repeatSubmit) {

    }

    /**
     * 环绕通知, 围绕着方法执行
     * 两种方式
     * 方式一：加锁 固定时间内不能重复提交
     * 方式二：先请求获取token，这边再删除token,删除成功则是第一次提交
     */
    @Around("pointCutNoRepeatSubmit(repeatSubmit)")
    public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String serviceId = repeatSubmit.serviceId();
        //用于记录成功或者失败
        boolean res = false;
        //防重提交类型
        String type = repeatSubmit.limitType().name();
        if (type.equalsIgnoreCase(RepeatSubmit.Type.PARAM.name())) {
            //方式一，参数形式防重提交
            //通过 redissonClient 获取分布式锁，基于IP地址、类名、方法名和服务ID生成唯一key
            long lockTime = repeatSubmit.lockTime();
            String ipAddr = AddrUtil.getRemoteAddr(request);
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            Method method = methodSignature.getMethod();
            String className = method.getDeclaringClass().getName();
            String key = serviceId + ":repeat_submit:" + MD5Util.generateMD5(String.format("%s-%s-%s-%s", ipAddr, className, method, serviceId));


            //使用 tryLock 尝试获取锁，如果无法获取（即锁已被其他请求持有），则认为是重复提交，直接返回null
            RLock lock = redissonClient.getLock(key);
            //锁自动过期时间为 lockTime 秒，确保即使程序异常也不会永久锁定资源,尝试加锁，最多等待0秒，上锁以后5秒自动解锁 [lockTime默认为5s, 可以自定义]
            res = lock.tryLock(0, lockTime, TimeUnit.SECONDS);
        } else {
            //方式二，令牌形式防重提交
            //从请求头中获取 request-token，如果不存在，则抛出异常
            String requestToken = request.getHeader("request-token");
            if (StringUtils.isBlank(requestToken)) {
                throw new RuntimeException("请求未包含令牌");
            }
            //使用 request-token 和 serviceId 构造Redis的key，尝试从Redis中删除这个键。如果删除成功，说明是首次提交；否则认为是重复提交
            String key = String.format(SERVICE_SUBMIT_TOKEN_KEY, repeatSubmit.serviceId(), requestToken);
            res = redisTemplate.delete(key);
        }
        if (!res) {
            System.out.println("请求重复提交");
            return null;
        }
        //在环绕通知的前后记录日志，有助于跟踪方法执行情况和重复提交的检测
        System.out.println("环绕通知执行前");
        Object obj = joinPoint.proceed();
        System.out.println("环绕通知执行后");
        return obj;
    }

}
